﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GameUtils;
using Microsoft.Xna.Framework;
using ThouShaltNotBrick.Logic.GeneralObjects;
using ThouShaltNotBrick.Logic.Helpers;

namespace ThouShaltNotBrick.Logic.Collisions
{
    public static class CollisionHelpers
    {
        #region Constants
        private static readonly List<Tuple<float, float>> PadReflectionValues = new List<Tuple<float, float>>
        {
            new Tuple<float, float>(-1.0f, 0.96f * MathHelper.Pi),
            new Tuple<float, float>(-0.5f, 0.8f * MathHelper.Pi),
            new Tuple<float, float>(0.5f, 0.2f * MathHelper.Pi),
            new Tuple<float, float>(1.0f, 0.04f * MathHelper.Pi)
        };
        #endregion

        #region Methods
        #region Pad
        public static float? CheckPadCollision(Ball pBall, Pad pPad)
        {
            float r = pBall.Radius;
            Vector2 c = pBall.Position;

            RectangleF padRect = pPad.GetBoundingRect();
            RectangleF xyInflatedPadRect = padRect;
            xyInflatedPadRect.Inflate(r, r);

            if (!xyInflatedPadRect.Contains(c))
            {
                return null;
            }

            Vector2 ballCenterAtContact = GetLineRectContactPoint(pBall.PrevPosition, c, xyInflatedPadRect);

            float normalizedCollisionOffset = 2.0f * (ballCenterAtContact.X - pPad.Position) / Constants.PAD_WIDTH;
            return MathHelper.Clamp(normalizedCollisionOffset, -1.0f, 1.0f);
        }

        public static float GetPadReflectDirection(float pPadNormalizedCollisionOffset)
        {
            float val = pPadNormalizedCollisionOffset;
            int afterIndex = PadReflectionValues.FindIndex(prv => prv.Item1 > val);

            if (afterIndex == 0)
            {
                return PadReflectionValues.First().Item2;
            }
            else if (afterIndex == -1)
            {
                return PadReflectionValues.Last().Item2;
            }
            else
            {
                return MathUtils.LinearInterpolation(
                    val,
                    PadReflectionValues[afterIndex - 1].Item1,
                    PadReflectionValues[afterIndex].Item1,
                    PadReflectionValues[afterIndex - 1].Item2,
                    PadReflectionValues[afterIndex].Item2);
            }
        }

        public static bool IsPadCollision(CollisionObject pCollisionObject)
        {
            return pCollisionObject != null && pCollisionObject.CollisionObjectType == CollisionObjectType.Pad;
        }
        #endregion

        #region Bounds
        public static CollisionSide CheckBoundsCollision(Ball pBall)
        {
            float positionX = pBall.Position.X;
            float positionY = pBall.Position.Y;

            RectangleF bounds = GameHelpers.GetBounds(pBall);

            CollisionSide collisionSide = CollisionSide.None;
            if (positionX <= bounds.Left)
            {
                collisionSide |= CollisionSide.Left;
            }
            else if (positionX >= bounds.Right)
            {
                collisionSide |= CollisionSide.Right;
            }

            if (positionY <= bounds.Bottom)
            {
                collisionSide |= CollisionSide.Bottom;
            }
            else if (positionY >= bounds.Top)
            {
                collisionSide |= CollisionSide.Top;
            }

            return collisionSide;
        }

        public static bool IsBoundsCollisionOnProvidedSide(CollisionObject pCollisionObject, CollisionSide pCollisionSide)
        {
            return pCollisionObject != null &&
                pCollisionObject.CollisionObjectType == CollisionObjectType.Bounds &&
                (pCollisionObject.CollisionSide & pCollisionSide) != 0;
        }
        #endregion

        #region Bricks
        public static CollisionSide CheckBrickCollision(
            Ball pBall,
            BrickWithCurrentState pBrick,
            BrickWithCurrentState[,] pBricks)
        {
            float r = pBall.Radius;
            Vector2 c = pBall.Position;
            Vector2 cPrev = pBall.PrevPosition;

            RectangleF brickRect = BrickHelpers.GetBrickRectangle(pBrick);
            RectangleF xyInflatedBrickRect = brickRect;
            xyInflatedBrickRect.Inflate(r, r);

            if (!xyInflatedBrickRect.Contains(c))
            {
                return CollisionSide.None;
            }

            Vector2 ballCenterAtContact = GetLineRectContactPoint(cPrev, c, xyInflatedBrickRect);

            CollisionSide side = CollisionSide.None;
            if (ballCenterAtContact.X < brickRect.Left && !IsBrickAtPosition(pBricks, pBrick.Row, pBrick.Column - 1))
            {
                side |= CollisionSide.Left;
            }
            else if (ballCenterAtContact.X > brickRect.Right && !IsBrickAtPosition(pBricks, pBrick.Row, pBrick.Column + 1))
            {
                side |= CollisionSide.Right;
            }
            if (ballCenterAtContact.Y < brickRect.Bottom && !IsBrickAtPosition(pBricks, pBrick.Row - 1, pBrick.Column))
            {
                side |= CollisionSide.Bottom;
            }
            else if (ballCenterAtContact.Y > brickRect.Top && !IsBrickAtPosition(pBricks, pBrick.Row + 1, pBrick.Column))
            {
                side |= CollisionSide.Top;
            }

            return side;
        }

        public static bool CanCollisionOccur(
            Ball pBall,
            BrickWithCurrentState pBrick)
        {
            int row = pBrick.Row;
            int col = pBrick.Column;
            CollisionObject prevCollision = pBall.PrevCollisionObject;
            return
                !IsBrickCollisionOnProvidedBrickPosition(prevCollision, row, col) &&
                !IsBrickCollisionOnProvidedBrickPosition(prevCollision, row, col - 1) &&
                !IsBrickCollisionOnProvidedBrickPosition(prevCollision, row, col + 1) &&
                !IsBrickCollisionOnProvidedBrickPosition(prevCollision, row - 1, col) &&
                !IsBrickCollisionOnProvidedBrickPosition(prevCollision, row + 1, col);
        }

        private static bool IsBrickCollisionOnProvidedBrickPosition(
            CollisionObject pCollisionObject,
            int pBrickRow,
            int pBrickColumn)
        {
            return pCollisionObject != null &&
                pCollisionObject.CollisionObjectType == CollisionObjectType.Brick &&
                pCollisionObject.BrickRow == pBrickRow &&
                pCollisionObject.BrickColumn == pBrickColumn;
        }

        private static bool IsBrickAtPosition(
            BrickWithCurrentState[,] pBricks,
            int pRow,
            int pColumn)
        {
            if (0 <= pRow && pRow < Constants.BRICK_ROWS &&
                0 <= pColumn && pColumn < Constants.BRICK_COLUMNS)
            {
                BrickWithCurrentState brick = pBricks[pRow, pColumn];
                return BrickHelpers.IsBrickExisting(brick);
            }
            else
            {
                return false;
            }
        }

        public static CollisionObject ResolveCollisionObject(
            Ball pBall,
            List<CollisionObject> pPossibleCollisionObjects)
        {
            if (pPossibleCollisionObjects == null || pPossibleCollisionObjects.Count == 0)
            {
                return null;
            }

            int ballBrickRow = BrickHelpers.GetCoordinateAsBrickRow(pBall.Position.Y);
            int ballBrickColumn = BrickHelpers.GetCoordinateAsBrickColumn(pBall.Position.X);

            List<CollisionObject> sameRowAndColumnBricks = pPossibleCollisionObjects.Where(
                co => co.BrickRow == ballBrickRow && co.BrickColumn == ballBrickColumn).ToList();
            if (sameRowAndColumnBricks.Count > 0)
            {
                return sameRowAndColumnBricks.First();
            }

            List<CollisionObject> sameColumnBricks = pPossibleCollisionObjects.Where(
                co => co.BrickColumn == ballBrickColumn).ToList();
            if (sameColumnBricks.Count > 0)
            {
                return sameColumnBricks.First();
            }

            List<CollisionObject> sameRowBricks = pPossibleCollisionObjects.Where(
                co => co.BrickRow == ballBrickRow).ToList();
            if (sameRowBricks.Count > 0)
            {
                return sameRowBricks.First();
            }

            return pPossibleCollisionObjects.First();
        }
        #endregion

        #region Private Utility Methods
        private static Vector2 GetLineRectContactPoint(
            Vector2 pA1,
            Vector2 pA2,
            RectangleF pRect)
        {
            Vector2 aVec = pA2 - pA1;

            Vector2 bl = pRect.BottomLeft;
            Vector2 br = pRect.BottomRight;
            Vector2 tl = pRect.TopLeft;
            Vector2 tr = pRect.TopRight;

            float t = 1.0f;
            LineLineIntersectionParameters ip;

            ip = GetLineLineIntersectionParameter(pA1, pA2, tl, bl);
            t = GetNewT(t, ip);
            ip = GetLineLineIntersectionParameter(pA1, pA2, tr, br);
            t = GetNewT(t, ip);
            ip = GetLineLineIntersectionParameter(pA1, pA2, bl, br);
            t = GetNewT(t, ip);
            ip = GetLineLineIntersectionParameter(pA1, pA2, tl, tr);
            t = GetNewT(t, ip);

            Vector2 contactPoint = pA1 + t * aVec;
            return contactPoint;
        }

        private static LineLineIntersectionParameters GetLineLineIntersectionParameter(
            Vector2 pA1,
            Vector2 pA2,
            Vector2 pB1,
            Vector2 pB2)
        {
            Vector2 aVec = pA2 - pA1;
            Vector2 bVec = pB2 - pB1;
            float aCrossB = Vector2Ex.Cross(aVec, bVec);

            // if lines are parallel, intersection parameter is infinite
            if (aCrossB == 0.0f)
            {
                return new LineLineIntersectionParameters(float.PositiveInfinity, float.PositiveInfinity);
            }

            Vector2 a1ToB1Vec = pB1 - pA1;
            float t1 = Vector2Ex.Cross(a1ToB1Vec, bVec) / aCrossB;
            float t2 = Vector2Ex.Cross(a1ToB1Vec, aVec) / aCrossB;

            return new LineLineIntersectionParameters(t1, t2);
        }

        private static float GetNewT(float pCurrT, LineLineIntersectionParameters pIp)
        {
            if (0.0f <= pIp.T1 && pIp.T1 < pCurrT && 0.0f <= pIp.T2 && pIp.T2 <= 1.0f)
            {
                return pIp.T1;
            }
            else
            {
                return pCurrT;
            }
        }
        #endregion
        #endregion

        #region Inner Classes
        private struct LineLineIntersectionParameters
        {
            public float T1;
            public float T2;

            public LineLineIntersectionParameters(
                float pT1,
                float pT2)
            {
                T1 = pT1;
                T2 = pT2;
            }
        }
        #endregion
    }
}